﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ProLinq.Projection.Configuration;
using ProLinq.Toolkit;

namespace ProLinq.Projection
{
	// TODO: add unit tests, especially for targetType
	internal class ProjectionMapper
	{
		#region Fields

		private List<Configuration.Projection> projections;

		#endregion

		#region Constructors

		public ProjectionMapper()
		{
			this.projections = new List<Configuration.Projection>();
		}

		public ProjectionMapper(IEnumerable<Configuration.Projection> projections)
		{
			this.projections = new List<Configuration.Projection>(projections);
		}

		#endregion

		#region Properties

		public List<Configuration.Projection> Projections
		{
			get { return projections; }
		}

		#endregion

		#region Methods

		private Object GetMemberValue(Object obj, MemberInfo memberInfo)
		{
			if (memberInfo is FieldInfo)
			{
				var fieldInfo = (FieldInfo) memberInfo;
				return fieldInfo.GetValue(obj);
			}
			if (memberInfo is PropertyInfo)
			{
				var propInfo = (PropertyInfo) memberInfo;
				return propInfo.GetValue(obj);
			}
			return null;
		}

		private void SetMemberValue(Object obj, MemberInfo memberInfo, Object value)
		{
			if (memberInfo is FieldInfo)
			{
				var fieldInfo = (FieldInfo) memberInfo;
				fieldInfo.SetValue(obj, value);
			}
			else if (memberInfo is PropertyInfo)
			{
				var propInfo = (PropertyInfo) memberInfo;
				propInfo.SetValue(obj, value);
			}
		}

		private Type GetMemberType(MemberInfo member)
		{
			if (member is FieldInfo)
				return ((FieldInfo) member).FieldType;
			if (member is PropertyInfo)
				return ((PropertyInfo) member).PropertyType;
			return null;
		}

		private IEnumerable Map(IEnumerable source, Type targetType)
		{
			foreach (object obj in source)
			{
				yield return this.Map(obj, targetType);
			}
		}

		public Object Map(Object obj, Type targetType)
		{
			if (obj == null)
			{
				return null;
			}

			//TODO: is it right? If we have T to T mapping we skip it with next line. This makes type enrichment use case not possible
			if (targetType.IsInstanceOfType(obj))
			{
				return obj;
			}

			Configuration.Projection projection =
				projections.FirstOrDefault(
					p => obj.GetType().Is(p.SourceType) && (targetType.IsAssignableFrom(p.DestinationType)));
			if (projection != null)
			{
				if (projection.Factory != null)
					return projection.Factory.DynamicInvoke(obj); // TODO: optimize mapping factory invokation
				var values = new List<object>();
				foreach (PropertyMap prop in projection.Map)
				{
					object val = GetMemberValue(obj, prop.SourceProperty);
					var destType = this.GetMemberType(prop.DestinationProperty);
					object mappedVal = this.Map(val, destType);
					values.Add(mappedVal);
				}

				ConstructorInfo ctr =
					projection.DestinationType.GetConstructors().FirstOrDefault(c => c.GetParameters().Count() == values.Count);
				if (ctr != null)
				{
					object resObj = ctr.Invoke(values.ToArray());
					return resObj;
				}
				else
				{
					object resObj = Activator.CreateInstance(projection.DestinationType);
					Int32 i = 0;
					foreach (PropertyMap prop in projection.Map)
					{
						SetMemberValue(resObj, prop.DestinationProperty, values[i]);
						i++;
					}
					return resObj;
				}
			}
			else if (targetType.IsEnumerable() && targetType != typeof (String))
			{
				var collection = obj as IEnumerable;
				if (collection != null)
				{
					var elemType = targetType.GetEnumerableType();
					var mappedCollection = this.Map(collection, elemType);

					MethodInfo castMethod = typeof (Enumerable).GetMethod("Cast").MakeGenericMethod(elemType);
					mappedCollection = (IEnumerable) castMethod.Invoke(null, new object[] {mappedCollection});

					return mappedCollection;
				}
			}
			
			throw new ProjectionException(String.Format("Mapping of type {0} to type {1} failed.", obj.GetType(), targetType));
		}

		#endregion
	}
}